// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Objective C support is only available on mac.
@TestOn('mac-os')
library;

import 'dart:ffi';

import 'package:objective_c/objective_c.dart';
import 'package:test/test.dart';

import 'util.dart';

void main() {
  group('NSDictionary', () {
    test('of', () {
      final obj1 = 'obj1'.toNSString();
      final obj2 = 'obj2'.toNSString();
      final obj3 = 'obj3'.toNSString();
      final obj4 = 'obj4'.toNSString();
      final obj5 = 'obj5'.toNSString();
      final obj6 = 'obj6'.toNSString();

      final dict = NSDictionary.of({
        obj1: obj2,
        obj3: obj4,
        obj5: obj6,
      }).asDart();

      expect(dict.length, 3);
      expect(dict[obj1], obj2);
      expect(dict[obj3], obj4);
      expect(dict[obj5], obj6);

      // Keys are copied, so compare the string values.
      final actualKeys = <String>[];
      for (final key in dict.keys) {
        actualKeys.add(NSString.as(key).toDartString());
      }
      expect(actualKeys, unorderedEquals(['obj1', 'obj3', 'obj5']));

      // Values are stored by reference, so we can compare the actual objects.
      final actualValues = <ObjCObject>[];
      for (final value in dict.values) {
        actualValues.add(value);
      }
      expect(actualValues, unorderedEquals([obj2, obj4, obj6]));
    });

    test('immutable', () {
      final obj1 = 'obj1'.toNSString();
      final obj2 = 'obj2'.toNSString();
      final obj3 = 'obj3'.toNSString();
      final obj4 = 'obj4'.toNSString();
      final obj5 = 'obj5'.toNSString();
      final obj6 = 'obj6'.toNSString();

      // NSDictionary.of actually returns a NSMutableDictionary, so our
      // immutability tests wouldn't actually work. So convert it to a real
      // NSDictionary using an ObjC constructor.
      final dict = NSDictionary.dictionaryWithDictionary(
        NSDictionary.of({obj1: obj2, obj3: obj4, obj5: obj6}),
      ).asDart();

      expect(() => dict[obj3] = obj1, throwsUnsupportedError);
      expect(dict.clear, throwsUnsupportedError);
      expect(() => dict.remove(obj1), throwsUnsupportedError);
    });

    test('MapBase mixin', () {
      final obj1 = 'obj1'.toNSString();
      final obj2 = 'obj2'.toNSString();
      final obj3 = 'obj3'.toNSString();
      final obj4 = 'obj4'.toNSString();
      final obj5 = 'obj5'.toNSString();
      final obj6 = 'obj6'.toNSString();

      final dict = NSDictionary.of({
        obj1: obj2,
        obj3: obj4,
        obj5: obj6,
      }).asDart();

      expect(dict.isNotEmpty, isTrue);
      expect(dict.containsKey(obj1), isTrue);
      expect(dict.containsKey(obj4), isFalse);
      expect(dict.containsValue(obj2), isTrue);
      expect(dict.containsValue(obj3), isFalse);

      expect(
        dict.map((key, value) => MapEntry<ObjCObject, ObjCObject>(value, key)),
        {obj2: obj1, obj4: obj3, obj6: obj5},
      );
      expect(
        dict.keys.map((key) => NSString.as(key).toDartString()).toList(),
        unorderedEquals(['obj1', 'obj3', 'obj5']),
      );
      expect(dict.values.toList(), unorderedEquals([obj2, obj4, obj6]));
    });

    test('ref counting', () async {
      final pointers = <Pointer<ObjCObjectImpl>>[];
      Map<NSCopying, ObjCObject>? map;

      autoReleasePool(() {
        // The dictionary key has to implement NSCopying. NSString is used in
        // the other tests because it's easy to construct. But it isn't ref
        // counted in the same way as other objects, so here we use NSArray.
        final obj1 = NSArray.of(['apple'.toNSString()]);
        final obj2 = NSObject();
        final obj3 = NSArray.of(['banana'.toNSString()]);
        final obj4 = NSObject();
        final obj5 = NSArray.of(['carrot'.toNSString()]);
        final obj6 = NSObject();
        final objects = {obj1: obj2, obj3: obj4, obj5: obj6};
        final objCMap = NSDictionary.of(objects);
        map = objCMap.asDart();

        pointers.addAll(map!.keys.map((o) => o.ref.pointer));
        pointers.addAll(map!.values.map((o) => o.ref.pointer));
        pointers.add(objCMap.ref.pointer);

        for (final pointer in pointers) {
          expect(objectRetainCount(pointer), greaterThan(0));
        }
      });

      doGC();
      await Future<void>.delayed(Duration.zero);
      doGC();
      for (final pointer in pointers) {
        expect(objectRetainCount(pointer), greaterThan(0));
      }
      map = null;

      doGC();
      await Future<void>.delayed(Duration.zero);
      doGC();
      for (final pointer in pointers) {
        expect(objectRetainCount(pointer), 0);
      }
    });
  });
}
